#include "lua_entity.hh"
#include "lua_bind.hh"
#include "../../plugin/base/rpc_base.hh"

static luaL_Reg entityLibrary[] = { 
    { "impl", &LuaEntity::newLuaEntity },
    { nullptr, nullptr } 
};

LuaEntity::LuaEntity()
    : luaDisposed_(false) {
    metaKey_ = std::to_string(reinterpret_cast<uint64_t>(this));
}

void LuaEntity::installEntityLibrary(lua_State*) {
    auto vm = LuaRef.getVirtualMachine();
    StackBalancer sb(vm);
    // Register methods
    luaL_register(vm, "entity", entityLibrary);
}

// Copy RPC implementation to entity's metatable
// @param L Lua thread
// @param index The index of implementation in stack
// @param entity LuaEntity
static void copyTable(lua_State* L, int index, LuaEntity* entity) {
    lua_pushnil(L);
    while (lua_next(L, index)) {
        auto key = lua_tostring(L, -2);
        lua_setfield(L, -3, key);
    }
}

// Create entity's metatable and initialize it
static void createMetaTable(lua_State* L, LuaEntity* entity, int nargs) {
    auto key = std::to_string((uint64_t)entity);
    luaL_newmetatable(L, key.c_str());
    // __gc
    lua_pushcfunction(L, &LuaEntity::deleteLuaEntity);
    lua_setfield(L, -2, "__gc");
    // __index
    lua_pushvalue(L, -1);
    lua_setfield(L, -2, "__index");
    // __newindex
    lua_pushcfunction(L, &LuaEntity::luaAssign);
    lua_setfield(L, -2, "__newindex");
    // resolve
    lua_pushcfunction(L, &LuaEntity::luaResolve);
    lua_setfield(L, -2, "resolve");
    // dispose
    lua_pushcfunction(L, &LuaEntity::luaDispose);
    lua_setfield(L, -2, "dispose");
    // copy the method from RPC's implementations
    for (int i = 2; i <= nargs; i++) {
        copyTable(L, i, entity);
    }
    lua_pop(L, 1);
    // Create userdata for LuaEntity
    auto address = (LuaEntity**)lua_newuserdata(L, sizeof(LuaEntity*));
    *address = entity;
    luaL_setmetatable(L, key.c_str());
}

int LuaEntity::newLuaEntity(lua_State* L) {
    auto nargs = lua_gettop(L);
    if (nargs <= 1) {
        raiseError(L, "impl() need more than 2 parameters");
        return 0;
    }
    if (!lua_isinteger(L, 1)) {
        raiseError(L, "impl() first parameter should be entity type");
        return 0;
    }
    auto type = (EntityType)lua_tointeger(L, 1);
    for (int i = 2; i <= nargs; i++) {
        if (!lua_istable(L, i)) {
            raiseError(L, "impl() need only table for implementation");
            return 0;
        }
    }
    auto entity = createEntityByType<LuaEntity>(type);
    if (!entity) {
        raiseError(L, "impl() create entity failed");
        return 0;
    }
    // create metatable for LuaEntity
    createMetaTable(L, entity, nargs);
    // return the userdata
    return 1;
}

bool LuaEntity::isLuaDisposed() {
    return luaDisposed_;
}

void LuaEntity::removeMetatable(lua_State* L) {
    if (!luaDisposed_) {
        if (luaL_getmetatable(L, metaKey_.c_str())) {
            lua_pushnil(L);
            lua_setfield(L, LUA_REGISTRYINDEX, metaKey_.c_str());
        }
    }
}

int LuaEntity::deleteLuaEntity(lua_State* L) {
    StackBalancer sb(L);
    auto entity = *(LuaEntity**)(lua_touserdata(L, 1));
    entity->removeMetatable(L);
    entity->dispose();
    return 0;
}

int LuaEntity::luaResolve(lua_State* L) {
    if (!lua_isinteger(L, -1)) {
        raiseError(L, "Entity ID must be an integer");
        return 0;
    }
    auto address = (LuaEntity**)lua_touserdata(L, 1);
    if (!address) {
        raiseError(L, "Entity not found");
        return 0;
    }
    auto entity = *address;
    if (entity->isLuaDisposed()) {
        raiseError(L, "Access disposed entity");
        return 0;
    }
    auto id = (std::uint64_t)lua_tointeger(L, -1);
    bool ret = entity->resolve(id);
    lua_pushboolean(L, ret ? 1 : 0);
    return 1;
}

void LuaEntity::pushMetatable(lua_State* L) {
    luaL_getmetatable(L, metaKey_.c_str());
}

void LuaEntity::addAttribute(lua_State* L) {
    pushMetatable(L);
    lua_pushvalue(L, 2); // key
    lua_pushvalue(L, 3); // value
    lua_settable(L, -3); // set to metatable
}

int LuaEntity::luaAssign(lua_State * L) {
    auto address = (LuaEntity**)lua_touserdata(L, 1);
    auto entity = *address;
    if (entity->isLuaDisposed()) {
        raiseError(L, "Access disposed entity");
        return 0;
    }
    entity->addAttribute(L);
    return 0;
}

void LuaEntity::luaDispose() {
    luaDisposed_ = true;
}

int LuaEntity::luaDispose(lua_State * L) {
    auto entity = *(LuaEntity**)(lua_topointer(L, -1));
    if (entity->isLuaDisposed()) {
        raiseError(L, "Access disposed entity");
        return 0;
    }
    entity->removeMetatable(L);
    entity->luaDispose();
    return 0;
}

bool LuaEntity::callLuaImplMethod(PathID pathID, PathID fromPathID, std::uint64_t UUID, std::uint64_t RPCID,
    const char * interface, const char * method, const google::protobuf::Message & req, google::protobuf::Message & ack) {
    auto& luaBind = LuaRef;
    // Gets a thread
    auto coi = luaBind.getCoManager().getFree();
    if (!coi) {
        fatallog << "Fork Lua coroutine failed" << endlog;
        return false;
    }
    lua_State* L = *coi;
    StackBalancer sb(L);
    pushMetatable(L); // get userdata of entity
    lua_pushstring(L, interface);
    lua_gettable(L, -2); // get RPC interface table
    lua_pushstring(L, method);
    lua_gettable(L, -2); // get rpc method
    pushMetatable(L); // get userdata of entity
    toLua(L, req); // get the request for RPC call
    // stack -> ... method, userdata, request
    // Call Lua RPC method
    auto status = lua_resume(L, nullptr, 2);
    switch (status) {
        case LUA_OK:
            if (lua_istable(L, -1)) {
                // Gets ACK
                copyTableLuaToC(L, ack);
                // Free coroutine
                luaBind.getCoManager().addFree(coi);
            }
            return true;
        case LUA_YIELD:
            // Method yield and waiting for resume
            luaBind.getCallManager().add(coi, &ack, pathID, UUID, RPCID, fromPathID);
            // Add to busy map
            luaBind.getCoManager().addBusy(coi);
            return false;
        default:
            // Error
            catchError(L, status);
            // Delete ACK
            RPCRef.deletePBProtoMessage(ack);
            // Terminate coroutine
            luaBind.getCoManager().remove(coi);
            return false;
    }
}
